package com.alili_tv.system.controller;

import cn.hutool.core.util.IdUtil;
import com.alili_tv.system.common.BaseContext;
import com.alili_tv.system.common.LoginRequired;
import com.alili_tv.system.common.R;
import com.alili_tv.system.entity.User;
import com.alili_tv.system.entity.Video;
import com.alili_tv.system.exception.BusinessException;
import com.alili_tv.system.exception.SystemException;
import com.alili_tv.system.service.UserService;
import com.alili_tv.system.utils.CommonUtils;
import com.alili_tv.system.utils.SendMsgUtils;
import com.alili_tv.system.utils.ValidUtils;
import com.aliyuncs.exceptions.ClientException;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import jakarta.annotation.Resource;
import jakarta.mail.internet.AddressException;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.catalina.connector.ClientAbortException;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.support.PropertiesLoaderUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.http.HttpHeaders;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.io.*;
import java.util.Properties;
import java.util.Random;
import java.util.concurrent.TimeUnit;

@RestController
@RequestMapping(value = "/common", produces = "application/json")
@Slf4j
public class CommonController {

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    @Resource
    private SendMsgUtils sendMsgUtils;

    @Resource
    private UserService userService;

    public static String headshotPath;

    public static String videoPath;

    public static String coverPath;

    /**
     * 在构造方法中动态设置文件存放位置
     *
     * @throws IOException
     */
    public CommonController() throws IOException {
        ClassPathResource resource = new ClassPathResource("path.properties");
        Properties properties = PropertiesLoaderUtils.loadProperties(resource);
        if (CommonUtils.isWindows()) {
            headshotPath = properties.getProperty("data.path.windows.headshot");
            videoPath = properties.getProperty("data.path.windows.video");
            coverPath = properties.getProperty("data.path.windows.cover");
        } else if (CommonUtils.isLinux()) {
            headshotPath = properties.getProperty("data.path.linux.headshot");
            videoPath = properties.getProperty("data.path.linux.video");
            coverPath = properties.getProperty("data.path.linux.cover");
        } else {
            throw new SystemException("未知系统");
        }

        File videoFile = new File(videoPath);
        File headshotFile = new File(headshotPath);
        File coverFile = new File(coverPath);
        if (!videoFile.exists()) {
            videoFile.mkdirs();
        }
        if (!headshotFile.exists()) {
            headshotFile.mkdirs();
        }
        if (!coverFile.exists()) {
            coverFile.mkdirs();
        }
    }


    /**
     * 通过邮箱或者手机号发送验证码
     *
     * @param account 必须包含手机号或者邮箱号
     * @throws UnsupportedEncodingException
     * @throws AddressException
     */
    @GetMapping("/sendCode/{account}")
    public R<String> sendCode(@PathVariable String account) throws UnsupportedEncodingException, AddressException, ClientException {
        if (account == null || account.trim().equals(""))
            return R.error("手机号或邮箱不能为空");

        // 生成随机6位数验证码
        String code = String.valueOf(new Random().nextInt(900000) + 100000);

        // 两次请求验证码不能小于1分钟，否则请求错误
        Long expire = redisTemplate.getExpire(account);
        if (expire != null && expire > 240) {
            return R.error("验证码请求过快，请稍后重试");
        }

        // 调用工具包发送验证码
        if (ValidUtils.isValidEmail(account)) {
//            sendMsgUtils.sendEmail(access, code);
            new Thread(() -> {
                try {
                    // 调用阿里巴巴的短信接口
                    sendMsgUtils.sendEmail(account, code);
                } catch (UnsupportedEncodingException | AddressException e) {
                    throw new RuntimeException(e);
                }
            }).start();

        } else if (ValidUtils.isValidPhone(account)) {
            // 短信要钱、没钱了
//            sendMsgUtils.sendSMS(user.getPhone(), code);
        } else return R.error("手机号或邮箱错误");

        // 将验证码存入redis string（key：value = 账号 ： 验证码）5分钟
        redisTemplate.opsForValue().set(account, code, 5L, TimeUnit.MINUTES);

        log.info("发送验证码：{}  ：  {}", account, code);

        return R.success("发送成功，有效期为5分钟，请注意查收");
    }


    /**
     * 文件下载
     *
     * @param fileName
     * @param response
     */
    @GetMapping("/download/headshot/{fileName}")
    public void getHeadshot(@PathVariable String fileName, HttpServletResponse response) throws IOException {
        String fileUrl = headshotPath + fileName;
        downloadSimple(fileUrl, response);
    }

    @GetMapping("/download/cover/{fileName}")
    public void getCover(@PathVariable String fileName, HttpServletResponse response) throws IOException {
        String fileUrl = coverPath + fileName;
        downloadSimple(fileUrl, response);
    }

    private void downloadSimple(String fileUrl, HttpServletResponse response) throws IOException {
        FileInputStream fileInputStream;
        File file = new File(fileUrl);
        if (file.exists()) {
            fileInputStream = new FileInputStream(file);
        } else {
            fileInputStream = new FileInputStream(coverPath + "default.png");
        }

        // 通过输出流将文件写回浏览器，在浏览器展示图片
        ServletOutputStream outputStream = response.getOutputStream();

        response.setContentType("image/jpeg");

        int len;
        byte[] bytes = new byte[1024];
        while ((len = fileInputStream.read(bytes)) != -1) {
            outputStream.write(bytes, 0, len);
            outputStream.flush();
        }

        // 关闭资源
        outputStream.close();
        fileInputStream.close();
    }


    /**
     * 获取视频资源，实现断点续传
     * 为什么使用断点续传：直接返回整个视频时，小视频问题不大，视频大的话会卡顿很久，查看请求发现会先请求下载完整视频后才开始播放。
     * 断点续传过程：video会先发送一个探测请求获取视频的大小、长度等信息，之后才开始进行播放。
     * http请求有相关断点传输的协议：
     * 1. 状态码206：实现断点传输的协议
     * 2. 请求头： Range: bytes=first-end （指定获取数据的范围，first：开始数据的索引位置，end：结束数据的索引位置）
     * 3. 响应头：content-range: bytes first-end/length（指定响应范围）
     * 3. 响应头：content-length：（响应的长度）
     * 所以实现视频断点播放的大概思路为，
     * 先识别http请求头是否携带range请求头，如果携带则表示请求需要进行断点下载，
     * 识别出断点的位置后使用随机读取文件类RandomAccessFile进行一定范围的读写，
     * 之后返回设置Content-Range请求头表示返回数据的范围，并设置http状态码为206表示断点传输。
     *
     * @param response
     * @param range    必须带有请求头range
     * @throws IOException
     */
    @GetMapping("/download/video/{fileName}")
    public void getVideoSource(@PathVariable String fileName, HttpServletResponse response, @RequestHeader(HttpHeaders.RANGE) String range) throws IOException {
        RandomAccessFile targetFile = null;
        OutputStream outputStream = null;
        try {
            outputStream = response.getOutputStream();
            response.reset();

            //打开文件
            File file = new File(videoPath + fileName);
            if (!file.exists())
                throw new BusinessException("文件路径有误");

            // 读取文件
            targetFile = new RandomAccessFile(file, "r");
            long fileLength = targetFile.length();
            // 分段下载视频
            // 从range中提取数据的开始位置
            // DPlayer的Range格式：bytes=262144- （没有结束位置）
            long requestStart = Long.parseLong(range.substring(range.indexOf("=") + 1, range.indexOf("-")));

            // 断点传输下载视频返回206
            response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);

            // 根据协议设置请求头
            response.setHeader(HttpHeaders.ACCEPT_RANGES, "bytes");
            response.setHeader(HttpHeaders.CONTENT_TYPE, "video/mp4");
            response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "*"); // 允许跨域
            response.setHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(fileLength - requestStart));
            response.setHeader(HttpHeaders.CONTENT_RANGE, "bytes " + requestStart + "-" + (fileLength - 1) + "/"
                    + fileLength);

            // 设置targetFile，从自定义位置开始读取数据
            targetFile.seek(requestStart);

            // 从磁盘读取数据流返回
            byte[] cache = new byte[4096];
            while (fileLength > 0) {
                int len = targetFile.read(cache);
                if (fileLength < cache.length) {
                    outputStream.write(cache, 0, (int) fileLength);
                } else {
                    outputStream.write(cache, 0, len);
                    if (len < cache.length) {
                        break;
                    }
                }
                fileLength -= cache.length;
            }

            outputStream.flush();
        } catch (ClientAbortException e) {
            // tomcat原话。写操作IO异常几乎总是由于客户端主动关闭连接导致，所以直接吃掉异常打日志
            // 比如使用video播放视频时经常会发送Range为0- 的范围只是为了获取视频大小，之后就中断连接了
            log.info(e.getMessage());
        } finally {
            if (outputStream != null) {
                try {
                    outputStream.close();
                } catch (ClientAbortException ignored) {
                }
            }
            if (targetFile != null) {
                try {
                    targetFile.close();
                } catch (ClientAbortException ignored) {
                }
            }
        }
    }

    /**
     * 上传视频前检查用户是否还有未完成上传的视频
     *
     * @return
     */
    @LoginRequired
    @GetMapping("/tmpUpload")
    public R<Video> getTmpVideo() {
        ValueOperations<String, Object> ops = redisTemplate.opsForValue();
        String videoKey = BaseContext.getCurrentId() + "_video";
        String videoName = (String) ops.get(videoKey);
        String coverName = (String) ops.get(BaseContext.getCurrentId() + "_cover");

        Video video = new Video();
        video.setVideoPath(videoName);
        video.setCoverPath(coverName);
        return R.success(video);
    }

    /**
     * 上传视频
     *
     * @param file
     * @return 生成的文件名
     * @throws IOException
     */
    @LoginRequired
    @PostMapping("/upload/video")
    public R<String> uploadVideo(MultipartFile file) throws IOException {
        ValueOperations<String, Object> ops = redisTemplate.opsForValue();
        String key = BaseContext.getCurrentId() + "_video";
        String videoName = (String) ops.get(key);

        // 检查用户之前否还有未完成上传的视频，存在则删除
        if (videoName != null) {
            File oldFile = new File(videoPath + videoName);
            // 路径为文件且不为空则进行删除
            if (oldFile.isFile() && oldFile.exists()) {
                boolean delete = oldFile.delete();
            }
        }

        if (file.getSize() / 1024 / 1024 > 100)
            return R.error("视频大小不能超过100MB");

        String filename = file.getOriginalFilename();
        if (filename == null)
            return R.error("视频格式有误");

        if (!filename.contains(".mp4"))
            return R.error("暂不支持mp4以外格式的视频");

        // 重新设置文件名 hutool工具包
        filename = IdUtil.simpleUUID() + ".mp4";

        // 保存文件
        file.transferTo(new File(videoPath + filename));

        // 缓存视频文件名
        ops.set(key, filename);

        return R.success(filename);
    }

    /**
     * 上传封面
     *
     * @param file
     * @return
     * @throws IOException
     */
    @LoginRequired
    @PostMapping("/upload/cover")
    public R<String> uploadCover(MultipartFile file) throws IOException {
        if (file.getSize() / 1024 / 1024 > 2)
            return R.error("图片大小不能超过2MB");

        String filename = file.getOriginalFilename();
        if (filename == null)
            return R.error("图片格式有误");

        // 查找缓存的图片名
        ValueOperations<String, Object> ops = redisTemplate.opsForValue();
        String key = BaseContext.getCurrentId() + "_cover";
        String coverName = (String) ops.get(key);

        // 删除旧文件
        if (coverName != null) {
            File oldFile = new File(coverPath + coverName);
            System.out.println(oldFile.isFile() + " : " + oldFile.exists());
            if (oldFile.isFile() && oldFile.exists()) {
                boolean delete = oldFile.delete();
            }
        }

        // 重新设置文件名
        filename = IdUtil.simpleUUID() + filename.substring(filename.lastIndexOf("."));

        // 保存文件
        file.transferTo(new File(coverPath + filename));

        // 缓存文件名
        ops.set(key, filename);

        return R.success(filename);
    }

    /**
     * 更改头像
     *
     * @param file
     * @return
     * @throws IOException
     */
    @LoginRequired
    @PostMapping("/upload/headshot")
    public R<String> uploadHeadshot(MultipartFile file) throws IOException {
        if (file.getSize() / 1024 / 1024 > 2)
            return R.error("图片大小不能超过2MB");

        String filename = file.getOriginalFilename();
        if (filename == null)
            return R.error("图片格式有误");

        // 从数据库中查找旧头像名
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper
                .select(User::getHeadshot)
                .eq(User::getId, BaseContext.getCurrentId());
        User one = userService.getOne(queryWrapper);
        // 删除旧头像
        if (one != null && one.getHeadshot() != null) {
            File headshotFile = new File(headshotPath + one.getHeadshot());
            if (headshotFile.isFile() && headshotFile.exists()) {
                boolean delete = headshotFile.delete();
            }
        }

        // 重新设置文件名
        filename = IdUtil.simpleUUID() + filename.substring(filename.lastIndexOf("."));

        // 保存文件
        file.transferTo(new File(headshotPath + filename));

        // 更新数据库
        User user = new User();
        user.setId(BaseContext.getCurrentId());
        user.setHeadshot(filename);
        userService.updateById(user);

        return R.success(filename);
    }
}
